// TODO: rename to ItemsPanel, or something like that

#include <wx/wx.h>
#include <wx/statline.h>
#include <wx/evtloop.h>
#include <wx/clipbrd.h>
#include <wx/textctrl.h>
#include <wx/font.h>

#include "notes_panel_base.h"
#include "app_frame.h"
#include "settings.h"
#include "util.h"
#include "text_layout.h"
#include "unicode.h"

#define NOTE_LEFT_ALIGN 25
#define SEPARATOR_HEIGHT 10

#define GTKFIX_EVENT_ID 5252

#if defined TARGET_WIN32
void wxGetCharSize(WXHWND wnd, int *x, int *y, const wxFont& the_font);
#endif

enum {
    ID_POPUP_EDIT,
    ID_POPUP_COPY,
    ID_POPUP_DELETE
};

NotesPanelBase::NotesPanelBase(AppFrame *top_frame, wxWindow* parent)
    :wxScrolledCanvas(parent, -1, wxDefaultPosition, wxDefaultSize, wxHSCROLL)
{
    display_ok = false;
    dbgenter("NotesPanelBase::NotesPanelBase()\n");
    selection_index = -1;
    selected_item = NULL;
    this->top_frame = top_frame;

    render_options.show_keyword_marks = false;
    render_options.show_unknown_keywords = true;
    render_options.show_sources = true;
    render_options.collapse = false;
    render_options.highlight = false;

    this->Connect(                  wxEVT_SIZE,       wxSizeEventHandler(NotesPanelBase::OnSize), 0, this);
    this->Connect(                  wxEVT_KEY_DOWN,   wxKeyEventHandler(NotesPanelBase::OnKeyDown), 0, this);
    this->Connect(GTKFIX_EVENT_ID,  wxEVT_UPDATE_UI,  wxUpdateUIEventHandler(NotesPanelBase::GTKFix), 0, this);

    SetScrollRate(10, config->scroll_speed);

    SetBackgroundColour(wxColour(255, 255, 255));
    FitInside();
}

void NotesPanelBase::SetSelection(int new_index)
{
    dbgenter("NotesPanelBase::SetSelection(%d)\n", new_index);
    if (new_index<0 || new_index >= (int)disp_items.size()) {
        new_index = -1;
    }
    wxASSERT(items.size() == disp_items.size());
    wxASSERT(selection_index == -1 || (selection_index >= 0 && selection_index < (int)disp_items.size()));

    // no need to repaint selection if it remains
    // (SetSelection is sometimes used to reposition view after contents changed but selection remained)
    if (selection_index != new_index) {

        // if there is active selection, disable it
        if (selection_index != -1) {
            selected_item->wxtext->SetBackgroundColour(*wxWHITE);
            items[selection_index].Render(selected_item->wxtext, render_options);
        }

        // set new selection
        selection_index = new_index;

        // display selection
        if (new_index != -1) {
            wxASSERT(new_index >= 0 && new_index < (int)disp_items.size());
            selected_item = disp_items[new_index];
            selected_item->wxtext->SetBackgroundColour(wxColour(235, 235, 235));
            items[selection_index].Render(selected_item->wxtext, render_options);
        }
    }

    // move window to the selection
    if (new_index == -1) {
        Scroll(0, 0);
    } else {
        int scrunit_x, scrunit_y, view_x, view_y;
        GetScrollPixelsPerUnit(&scrunit_x, &scrunit_y);
        GetViewStart(&view_x, &view_y);
        view_x *= scrunit_x; view_y *= scrunit_y;

        int text_x, text_y;
        selected_item->wxtext->GetPosition(&text_x, &text_y);
        CalcUnscrolledPosition(text_x, text_y, &text_x, &text_y);
        int text_end_y = text_y + selected_item->wxtext->GetSize().y;

        // scroll down if bottom of selected item is under bottom of view
        dbg("view_y = %d, GetClientSize().y = %d, text_end_y = %d\n", view_y, GetClientSize().y, text_end_y);
        if (view_y + GetClientSize().y <= text_end_y) {
            Scroll(0, (text_end_y+5 - GetClientSize().y + scrunit_y-1) / scrunit_y);
            GetViewStart(&view_x, &view_y);
            view_x *= scrunit_x; view_y *= scrunit_y;
        }

        // scroll up if top of selected item is above top of view
        if (view_y > text_y)
            Scroll(0, (text_y - scrunit_y+1)/ scrunit_y);

        selected_item->wxtext->GetPosition(&text_x, &text_y);
    }
}

void NotesPanelBase::OnClick(wxMouseEvent& event)
{
    dbgenter("NotesPanelBase::OnClick()\n");
    DispItem *dispitem = (DispItem*)event.m_callbackUserData;
    SetSelection(dispitem->index);

    // allow selecting text
    event.Skip();
}

void NotesPanelBase::OnDoubleClick(wxMouseEvent& event)
{
    dbgenter("NotesPanelBase::OnDoubleClick()\n");
    DispItem *dispitem = (DispItem*)event.m_callbackUserData;

    EditItem(dispitem->index);
}

void NotesPanelBase::OnRightClick(wxMouseEvent& event)
{
    dbgenter("NotesPanelBase::OnRightClick()\n");
    DispItem *dispitem = (DispItem*)event.m_callbackUserData;

    wxMenu menu;
    //menu.SetTitle(GetItemTitle(this_item->index));
    menu.SetClientData(dispitem);
    menu.Append(ID_POPUP_EDIT,   _("&Edit"));
    menu.Append(ID_POPUP_COPY,   _("&Copy"));
    menu.Append(ID_POPUP_DELETE, _("&Delete"));
    menu.Connect(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(NotesPanelBase::OnPopupClick), NULL, this);
    PopupMenu(&menu);
}

void NotesPanelBase::OnPopupClick(wxCommandEvent& event)
{
    dbgenter("NotesPanelBase::OnPopupClick()\n");
    DispItem *dispitem = static_cast<DispItem*>(static_cast<wxMenu*>(event.GetEventObject())->GetClientData());
    switch(event.GetId()) {
        case ID_POPUP_EDIT:
            EditItem(dispitem->index);
            break;
        case ID_POPUP_COPY:
            CopyItemToClipboard(dispitem->index);
            break;
        case ID_POPUP_DELETE:
            DeleteItem(dispitem->index);
            break;
        default:
            wxFAIL_MSG("Unknown popup click ID");
    }
}

static int prev_size_x=0, prev_size_y=0;    //TODO: do this in some normal way
void NotesPanelBase::OnSize(wxSizeEvent& event)
{
    if (prev_size_x == event.GetSize().x && prev_size_y == event.GetSize().y)
        return;
    prev_size_x = event.GetSize().x;
    prev_size_y = event.GetSize().y;

    dbgenter("NotesPanelBase::OnSize(size=%d:%d)\n", event.GetSize().x, event.GetSize().y);

    // only redraw if display is in okay state
    // (Size event can for example arrive inside ErrMsg inside DB access, while panel
    // items are being read)
    // TODO: rename 'display_ok' to something better, like 'XXX_consistent'
    // TODO: only resize, don't make full Update
    if (display_ok)
        UpdateDisplay();
}

void NotesPanelBase::ClearDisplay()
{
    dbgenter("NotesPanelBase::ClearDisplay()\n");
    selection_index = -1;
    selected_item = NULL;

    for (unsigned x=0; x<disp_items.size(); x++) {
        // make copy of DispItem object (it is callback data in handlers of wxTextCtrls,
        // so the original might get destroyed earlier than we are done).
        // TODO: this is probably wrong - call Close()?
        DispItem disp_item = *disp_items[x];
        wxASSERT(disp_item.wxtext);
        wxASSERT(disp_item.separator);
        disp_item.wxtext->Destroy();
        disp_item.separator->Destroy();
    }

    disp_items.clear();
}

void NotesPanelBase::UpdateDisplay()
{
    dbgenter("NotesPanelBase::UpdateDisplay()\n");
    int old_selection_index = selection_index;

    Freeze();
    display_ok = false;

    ClearDisplay();

    // wrap items text to this size
    dbg("setting screen width\n");
    SetVirtualSize(20, GetSize().y + 10);   // force vertical scrollbar to appear, so that GetClientSize subtracts its size
    wrap_width = GetClientSize().x -  NOTE_LEFT_ALIGN - 5;

    // fill screen with items
    dbg("Creating items\n");
    for (unsigned index=0; index<items.size(); index++) {

        //dbg("-create control\n");
        wxTextCtrl *wxtext = new wxTextCtrl(this, -1, wxEmptyString, wxDefaultPosition, wxDefaultSize,
                                            wxTE_READONLY | wxBORDER_NONE | wxTE_MULTILINE | wxTE_NO_VSCROLL | wxTE_RICH);
        wxtext->SetBackgroundColour(wxColor(255, 255, 255));
        wxtext->SetSize(NOTE_LEFT_ALIGN, -3001, wrap_width, 3000);  //high height to make sure we won't get scrollbared width in GTKFix
        wxtext->SetValue("");

        //dbg("-render\n");
        items[index].Render(wxtext, render_options);

        // add seperator
        //dbg("-rest\n");
        wxStaticLine *separator = new wxStaticLine(this, -1, wxPoint(0, 0), wxSize(200, 2));

        // create DispItem for this item
        DispItem *disp_item = new DispItem(wxtext, index, separator);
        disp_items.push_back(disp_item);

        // set up click handler, DispItem object as callback data
        // note: callback data is destroyed by wxWidgets upon destroying wxtext, so we can't use same object twice
        wxtext->Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(NotesPanelBase::OnClick), disp_item, this);
        wxtext->Connect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(NotesPanelBase::OnDoubleClick), new DispItem(*disp_item), this);
        wxtext->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(NotesPanelBase::OnRightClick), new DispItem(*disp_item), this);

        // in order to allow selecting text with mouse, wxTextCtrl must be able to receive focus
        // however, even when one has focus, we won't let it process keyboard input ourselves.
        wxtext->Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(NotesPanelBase::OnKeyDown), new DispItem(*disp_item), this);
    }

    // restore selection
    SetSelection(old_selection_index);

    Thaw();
    display_ok = true;

    dbg("Updated\n");

    CheckIntegrity();

    // force GTKFix method after message loop runs
    // wxUpdateUIEvent(GTKFIX_EVENT_ID) is used just as a bogus event.
    // TODO: try to use wxIdleEvent instead
    wxUpdateUIEvent event(GTKFIX_EVENT_ID);
    AddPendingEvent(event);
    needs_gtk_fix = true;
}

// This can't be done in UpdateDisplay, because for our GTK magic to work, events generated
// there must be processed. Until then, GTK widgets remain internally with their default size,
// and so we can't determine number of displayed (wrapped) lines for our width
// see: http://trac.wxwidgets.org/ticket/12366
void NotesPanelBase::GTKFix(wxUpdateUIEvent& event)
{
    if (!needs_gtk_fix) {
        dbg("NotesPanelBase::GTKFix() ignored\n");
        return;
    }
    needs_gtk_fix = false;

    dbgenter("NotesPanelBase::GTKFix()\n");

    Freeze();

    // For some reason, this procedure sometimes moved all items to lower (under wxGTK-2.8)
    // It is probably some kind of bug in wxTextCtrl::SetSize() with un/scrolled coords.
    // Scrolling to (0,0) seems to workaround this problem.
    int view_x, view_y;
    GetViewStart(&view_x, &view_y);
    Scroll(0, 0);

//    // get height of line
//#ifdef TARGET_WIN32
//    int dummy, text_line_height;
//    wxGetCharSize(this->GetHWND(), &dummy, &text_line_height, config->GetNoteFont());
//#else
//    int text_line_height = config->fonts[FONT_DEFAULT].attr.GetFont().GetPixelSize().y;
//#endif

    int pos_y = 0;

    // resize & move all notes
    for (int item_index=0; item_index < (int)disp_items.size(); item_index++) {

        // get wrapped height
#ifdef TARGET_WIN32
        int item_height = msw_wxTextCtrl_GetWrappedHeight(disp_items[item_index]->wxtext, wrap_width);
        dbg("height = %d\n", item_height);
#elif defined TARGET_LINUX
        int item_height = gtk_wxTextCtrl_GetWrappedHeight(disp_items[item_index]->wxtext);
#else
    #error
#endif

        // resize & reposition item
        int item_y = pos_y;
        disp_items[item_index]->wxtext->SetSize(NOTE_LEFT_ALIGN, item_y, wrap_width, item_height);
        pos_y += item_height;

        // reposition separator
        int separator_y = pos_y+(SEPARATOR_HEIGHT/2);
        disp_items[item_index]->separator->SetPosition(wxPoint(5, separator_y));
        pos_y += SEPARATOR_HEIGHT;

    }

    // set size of inner scrolled panel
    SetVirtualSize(GetClientSize().x, pos_y);

    // move window to current selection
    SetSelection(selection_index);

    CheckIntegrity();
    Thaw();
}

int NotesPanelBase::FindNoteIndexByID(int id)
{
    for (unsigned index=0; index < disp_items.size(); index++) {
        if (items[index].isNote() && items[index].note().id == id)
            return index;
    }
    return -1;
}

int NotesPanelBase::FindKeywordIndexByID(int id)
{
    for (unsigned index=0; index < disp_items.size(); index++) {
        if (items[index].isKeyword() && items[index].keyword().id == id)
            return index;
    }
    return -1;
}

void NotesPanelBase::EditItem(int index)
{
    dbgenter("NotesPanelBase::EditItem(%d)\n", index);
    wxASSERT(index != -1);
    wxASSERT(index>=0 && index<(int)items.size());
    TextItem& item = items[index];

    switch(item.type()) {
        case TextItem::INFO:
            wxBell();
            break;
        case TextItem::NOTE:
            top_frame->EditNoteDialog(item.note());
            break;
        case TextItem::KEYWORD:
            top_frame->EditKeywordDialog(item.keyword());
            break;
        default:
            wxFAIL_MSG("EditItem(): Unknown item type");
    }
}

void NotesPanelBase::DeleteItem(int index)
{
    dbgenter("NotesPanelBase::DeleteItem(%d)\n", index);
    wxASSERT(index != -1);
    wxASSERT(index>=0 && index<(int)items.size());

    switch(items[index].type()) {
        case TextItem::INFO:
            wxBell();
            break;
        case TextItem::NOTE:
            if (!top_frame->DeleteNote(items[index].note()))
                return;
            break;
        case TextItem::KEYWORD:
            if (!top_frame ->DeleteKeyword(items[index].keyword()))
                return;
            break;
        default:
            wxFAIL_MSG("Delete item: Unknown item type");
    }
}

int NotesPanelBase::FindNoteIndexByContent(const Note& n)
{
    wxASSERT(items.size() == disp_items.size());
    for (unsigned x=0; x<items.size(); x++) {
        if (items[x].isNote()) {
            Note& n2 = items[x].note();
            if (n2.manual_keywords == n.manual_keywords && n2.date == n.date && n2.text == n.text)
                return (int)x;
        }
    }
    return -1;
}

void NotesPanelBase::SelectNext()
{
    if (selection_index < (int)disp_items.size()-1)
        SetSelection(selection_index + 1);
}

void NotesPanelBase::SelectPrev()
{
    if (selection_index > 0)
        SetSelection(selection_index - 1);
}

void NotesPanelBase::OnKeyDown(wxKeyEvent& event)
{
    switch(event.GetKeyCode()) {
        case WXK_DOWN:
            if (event.GetModifiers() == wxMOD_SHIFT)
                ScrollDown();
            else if (event.GetModifiers() == wxMOD_NONE)
                SelectNext();
            else
                wxBell();
            return;
        case WXK_UP:
            if (event.GetModifiers() == wxMOD_SHIFT)
                ScrollUp();
            else if (event.GetModifiers() == wxMOD_NONE)
                SelectPrev();
            else
                wxBell();
            return;
        case WXK_RETURN:
            if (selection_index != -1)
                EditItem(selection_index);
            else
                wxBell();
            return;
        case WXK_DELETE:
            if (selection_index != -1)
                DeleteItem(selection_index);
            else
                wxBell();
            return;
        // C to toggle collapsed display
        // CTRL+C to copy item. This way we can't include this accelerator in menu, but if this was handled as normal menu
        // accelerator, regular CTRL+C wouldn't work inside edit boxes
        case 'c':
        case 'C':
            if (event.GetModifiers() == wxMOD_NONE) {
                render_options.collapse = !render_options.collapse;
                UpdateDisplay();
                return;
            }
            if (event.GetModifiers() == wxMOD_CONTROL) {
                if (selection_index != -1)
                    CopyItemToClipboard(selection_index);
                else
                    wxBell();
                return;
            }
            break;

        // toggle display of sources
        case 's':
        case 'S':
            if (event.GetModifiers() == wxMOD_NONE) {
                render_options.show_sources = !render_options.show_sources;
                UpdateDisplay();
            }
            break;

        // toggle display of unknown keywords
        case 'u':
        case 'U':
            if (event.GetModifiers() == wxMOD_NONE) {
                render_options.show_unknown_keywords = !render_options.show_unknown_keywords;
                UpdateDisplay();
            }
            break;

        // toggle highlight
        case 'h':
        case 'H':
            if (event.GetModifiers() == wxMOD_NONE) {
                render_options.highlight = !render_options.highlight;
                UpdateDisplay();
            }
            break;

        // toggle display of keyword marks
        case 'k':
        case 'K':
            if (event.GetModifiers() == wxMOD_NONE) {
                render_options.show_keyword_marks = !render_options.show_keyword_marks;
                UpdateDisplay();
            }
            break;

        // show full item information
        case 'i':
        case 'I':
            if (selection_index == -1) {
                wxBell();
                return;
            }
            wxMessageBox(GetItemInfo(selection_index), GetItemTitle(selection_index));
            break;
    }
    event.Skip();
}

bool NotesPanelBase::ContainsNoteID(int id) const
{
    for (int i=0; i<(int)items.size(); i++) {
        if (items[i].isNote() && items[i].note().id == id)
            return true;
    }
    return false;
}

bool NotesPanelBase::ContainsNote(const Note &n) const
{
    return ContainsNoteID(n.id);
}

void NotesPanelBase::InsertItemAt(const TextItem& item, unsigned index)
{
    dbgenter("NotesPanelBase::InsertItemAt(%d, %s)\n", index, item.ToString());

    wxASSERT(index <= items.size());
    wxASSERT(items.size() == disp_items.size());

    //dbg("selection_index=%d\n", selection_index);
    Freeze();

    // get Y position where new item starts
    int pos_y = 0;
    if (index == 0) {
        pos_y = 0;
    } else if (index == items.size()) {
        pos_y = disp_items[index-1]->wxtext->GetPosition().y + disp_items[index-1]->wxtext->GetSize().y + SEPARATOR_HEIGHT;
    } else {
        pos_y = disp_items[index]->wxtext->GetPosition().y;
    }

    // set up wrapped static text
    wxTextCtrl *static_text = new wxTextCtrl(this, -1, "", wxDefaultPosition, wxDefaultSize, wxTE_READONLY | wxBORDER_NONE | wxTE_MULTILINE | wxTE_NO_VSCROLL | wxTE_RICH);
    item.Render(static_text, render_options);
    //static_text->SetFont(config->GetFont(FONT_TEXT));
    static_text->SetBackgroundColour(wxColor(255, 255, 255));
    static_text->SetSize(NOTE_LEFT_ALIGN, -3001, wrap_width, 3000);  //high height to make sure we won't get scrollbared width in GTKFix

    // add seperator
    wxStaticLine *separator = new wxStaticLine(this, -1, wxPoint(5, 0), wxSize(200, 2));
    pos_y += SEPARATOR_HEIGHT;

    // create DispItem
    DispItem *disp_item = new DispItem(static_text, index, separator);
    disp_items.insert(disp_items.begin() + index, disp_item);

    // set up event handlers for wxTextCtrl
    static_text->Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(NotesPanelBase::OnClick),    disp_item, this);
    static_text->Connect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(NotesPanelBase::OnDoubleClick), disp_item, this);
    static_text->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(NotesPanelBase::OnRightClick), disp_item, this);

    // add to list of items
    items.insert(items.begin() + index, item);

    // increase indexes in rest of DispItems
    for (unsigned i=index+1; i<items.size(); i++)
        disp_items[i]->index++;

    // reset selection (no redrawing needed)
    selection_index = selection_index + ((int)index <= selection_index);

    // force also running GTKFix after messages are completed
    wxUpdateUIEvent event(GTKFIX_EVENT_ID);
    AddPendingEvent(event);
    needs_gtk_fix = true;

    Thaw();
}

void NotesPanelBase::RemoveNote(const Note& n)
{
    dbgenter("NotesPanelBase::RemoveNote(%s)\n", n.ToString());

    int index = FindNoteIndexByID(n.id);
    if (index != -1)
        RemoveItemAt((unsigned)index);
}

void NotesPanelBase::RemoveKeyword(const Keyword& k)
{
    dbgenter("NotesPanelBase::RemoveKeyword(%s)\n", k.ToString());

    int index = FindKeywordIndexByID(k.id);
    if (index != -1)
        RemoveItemAt((unsigned)index);
}

void NotesPanelBase::RemoveItemAt(int index)
{
    dbgenter("NotesPanelBase::RemoveItemAt(%d)\n", index);
    wxASSERT(index >= 0);
    wxASSERT(index < (int)items.size());
    wxASSERT(items.size() == disp_items.size());

    Freeze();

    // get size of item we will delete
    int deleted_item_height = disp_items[index]->wxtext->GetSize().y + SEPARATOR_HEIGHT;

    // remove item from lists
    assert(disp_items[index]);
    assert(disp_items[index]->wxtext);
    assert(disp_items[index]->separator);
    disp_items[index]->separator->Destroy();   // TODO: this crashes on wxMSW if it is behind wxtext->Destroy. WHY???
    disp_items[index]->wxtext->Destroy();                  // this also deletes DisplayedNote associated via Connect()
    disp_items.erase(disp_items.begin() + index);     // this only holds DisplayedNote*
    items.erase(items.begin() + index);

    // shift all following items upwards
    for (unsigned x=index; x<items.size(); x++) {
        int item_x, item_y, sep_x, sep_y;
        disp_items[x]->index--;
        disp_items[x]->wxtext->GetPosition(&item_x, &item_y);
        disp_items[x]->wxtext->SetPosition(wxPoint(item_x, item_y - deleted_item_height));
        disp_items[x]->separator->GetPosition(&sep_x, &sep_y);
        disp_items[x]->separator->SetPosition(wxPoint(sep_x, sep_y - deleted_item_height));
    }

    // update virtual size
    if (disp_items.size() > 0) {
        DispItem *last_item = disp_items[disp_items.size()-1];
        int total_height = last_item->wxtext->GetPosition().y + last_item->wxtext->GetSize().y + config->scroll_speed;
        SetVirtualSize(GetClientSize().x, total_height);
        dbg("Height = %u\n", total_height);
    } else {
        SetVirtualSize(GetClientSize().x, 0);
    }

    // adjust selection
    if (selection_index == index) {
        int new_index = selection_index - (selection_index == (int)items.size());
        selection_index = -1;  // previous selection is already deleted
        SetSelection(new_index);
    } else if (selection_index > (int)index)
        selection_index--;

    CheckIntegrity();

    Thaw();
}

void NotesPanelBase::ScrollDown()
{
    int scrunit_x, scrunit_y, view_x, view_y;
    GetScrollPixelsPerUnit(&scrunit_x, &scrunit_y);
    GetViewStart(&view_x, &view_y);
    Scroll(0, view_y + config->scroll_speed);
}

void NotesPanelBase::ScrollUp()
{
    int scrunit_x, scrunit_y, view_x, view_y;
    GetScrollPixelsPerUnit(&scrunit_x, &scrunit_y);
    GetViewStart(&view_x, &view_y);
    Scroll(0, view_y - config->scroll_speed);
}

void NotesPanelBase::CopyItemToClipboard(int index)
{
    dbgenter("NotesPanelBase::CopyItemToClipboard(%d)\n", index);
    wxASSERT(index != -1);
    wxASSERT(index>=0 && index<(int)items.size());

    wxASSERT(!wxTheClipboard->IsOpened());
    if (!wxTheClipboard->Open()) {
        ErrMsg(_("Failed to open clipboard"));
        return;
    }

    wxTextDataObject* data=0;
    wxString text;
    switch(items[selection_index].type()) {
        case TextItem::INFO:
            wxBell();
            return;
        case TextItem::NOTE: {
            Note& n = items[selection_index].note();
            if (!n.caption.IsEmpty()) text += n.caption + "\n";
            if (!n.date.IsEmpty())    text += n.date + "\n";
            text += n.text;
            data = new wxTextDataObject(text);
            break; }
        case TextItem::KEYWORD:
            ErrMsg(_("Keyword copying to clipboard not supported yet"));  //TODO
            return;
        default:
            wxFAIL_MSG("Unknown text item, can't copy to clipboard");
    }
    wxASSERT(data != 0);

    if (!wxTheClipboard->SetData(data)) {
        ErrMsg(_("Failed to set clipboard data"));
        wxTheClipboard->Close();
        return;
    }

    wxTheClipboard->Close();
}

void NotesPanelBase::LoadNotes(const vector<Note>& notes)
{
    for (unsigned x=0; x<notes.size(); x++)
        items.push_back(TextItem(notes[x]));
}

void NotesPanelBase::CheckIntegrity() const
{
    #ifdef _DEBUG
    wxASSERT(items.size() == disp_items.size());
    wxASSERT(items.size() > 0 || selection_index == -1);
    if (selection_index != -1) {
        wxASSERT(selection_index >= 0 && selection_index < (int)items.size());
        wxASSERT(selected_item->index == selection_index);
    }
    for (int i=0; i<(int)items.size(); i++) {
        wxASSERT(disp_items[i]->index == i);
    }
    #endif
}

wxString NotesPanelBase::GetItemTitle(int index) const
{
    wxASSERT(index != -1);
    wxASSERT(index>=0 && index<(int)items.size());
    switch(items[index].type()) {
        case TextItem::NOTE: {
            const Note& note = items[index].note();
            if (note.caption.IsEmpty())
                return wxFormat(_("Note #%d"), note.id);
            else
                return wxFormat(_("Note '%s'"), note.caption);
        }
        case TextItem::KEYWORD:
            return wxFormat(_("Keyword '%s'"), items[index].keyword().name);
        case TextItem::INFO:
            return "";
        default:
            wxFAIL_MSG("GetItemTitle(): Unknown item type");
    }
    return "";
}

wxString NotesPanelBase::GetItemInfo(int index) const
{
    wxASSERT(index != -1);
    wxASSERT(index>=0 && index<(int)items.size());

    wxString info;
    switch(items[index].type()) {
        case TextItem::NOTE: {
            const Note& n = items[selection_index].note();
            info = wxFormat(_("id: %d\ncaption: %s\ndate: %s\nsortdate: %s\n"), n.id, n.caption, n.date, ParseDate(n.date));
            info += wxFormat(_("created:\t%X\nmodified:\t%X\n"), n.created, n.modified);
            info += wxFormat(_("\nmanual keywords:\n%s\n\nkeywords:\n%s\n\ntext:\n%s\n\n"), n.manual_keywords, n.ParseAllKeywords(), n.text);
            return info;
        }
        case TextItem::KEYWORD: {
            const Keyword& k = items[selection_index].keyword();
            info = wxFormat(_("id: %d\nname: %s\nsortname: %s\n"), k.id, k.name, k.sortname);
            info += wxFormat(_("created:\t%X\nmodif:\t%X\nused:\t%X\n"), k.created, k.modified, k.used);
            info += wxFormat(_("\naliases:\n%s\n\nhints:\n%s\n\ndesc:\n%s"), k.aliases, k.hints, k.desc);
            return info;
        }
        case TextItem::INFO:
            return items[selection_index].info();
    }
    wxFAIL_MSG("GetItemInfo(): Unknown item type");
    return "";
}

void NotesPanelBase::NotifyChangedAppearance()
{
    dbgenter("NotesPanelBase::NotifyChangedAppearance()\n");
    UpdateDisplay();
}
